import { StartSSEOptions, StreamableHTTPClientTransport, StreamableHTTPReconnectionOptions } from '../../src/client/streamableHttp.js';
import { OAuthClientProvider, UnauthorizedError } from '../../src/client/auth.js';
import { JSONRPCMessage, JSONRPCRequest } from '../../src/types.js';
import { InvalidClientError, InvalidGrantError, UnauthorizedClientError } from '../../src/server/auth/errors.js';
import { type Mock, type Mocked } from 'vitest';

describe('StreamableHTTPClientTransport', () => {
    let transport: StreamableHTTPClientTransport;
    let mockAuthProvider: Mocked<OAuthClientProvider>;

    beforeEach(() => {
        mockAuthProvider = {
            get redirectUrl() {
                return 'http://localhost/callback';
            },
            get clientMetadata() {
                return { redirect_uris: ['http://localhost/callback'] };
            },
            clientInformation: vi.fn(() => ({ client_id: 'test-client-id', client_secret: 'test-client-secret' })),
            tokens: vi.fn(),
            saveTokens: vi.fn(),
            redirectToAuthorization: vi.fn(),
            saveCodeVerifier: vi.fn(),
            codeVerifier: vi.fn(),
            invalidateCredentials: vi.fn()
        };
        transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), { authProvider: mockAuthProvider });
        vi.spyOn(global, 'fetch');
    });

    afterEach(async () => {
        await transport.close().catch(() => {});
        vi.clearAllMocks();
    });

    it('should send JSON-RPC messages via POST', async () => {
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'test',
            params: {},
            id: 'test-id'
        };

        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 202,
            headers: new Headers()
        });

        await transport.send(message);

        expect(global.fetch).toHaveBeenCalledWith(
            expect.anything(),
            expect.objectContaining({
                method: 'POST',
                headers: expect.any(Headers),
                body: JSON.stringify(message)
            })
        );
    });

    it('should send batch messages', async () => {
        const messages: JSONRPCMessage[] = [
            { jsonrpc: '2.0', method: 'test1', params: {}, id: 'id1' },
            { jsonrpc: '2.0', method: 'test2', params: {}, id: 'id2' }
        ];

        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 200,
            headers: new Headers({ 'content-type': 'text/event-stream' }),
            body: null
        });

        await transport.send(messages);

        expect(global.fetch).toHaveBeenCalledWith(
            expect.anything(),
            expect.objectContaining({
                method: 'POST',
                headers: expect.any(Headers),
                body: JSON.stringify(messages)
            })
        );
    });

    it('should store session ID received during initialization', async () => {
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'initialize',
            params: {
                clientInfo: { name: 'test-client', version: '1.0' },
                protocolVersion: '2025-03-26'
            },
            id: 'init-id'
        };

        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 200,
            headers: new Headers({ 'content-type': 'text/event-stream', 'mcp-session-id': 'test-session-id' })
        });

        await transport.send(message);

        // Send a second message that should include the session ID
        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 202,
            headers: new Headers()
        });

        await transport.send({ jsonrpc: '2.0', method: 'test', params: {} } as JSONRPCMessage);

        // Check that second request included session ID header
        const calls = (global.fetch as Mock).mock.calls;
        const lastCall = calls[calls.length - 1];
        expect(lastCall[1].headers).toBeDefined();
        expect(lastCall[1].headers.get('mcp-session-id')).toBe('test-session-id');
    });

    it('should terminate session with DELETE request', async () => {
        // First, simulate getting a session ID
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'initialize',
            params: {
                clientInfo: { name: 'test-client', version: '1.0' },
                protocolVersion: '2025-03-26'
            },
            id: 'init-id'
        };

        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 200,
            headers: new Headers({ 'content-type': 'text/event-stream', 'mcp-session-id': 'test-session-id' })
        });

        await transport.send(message);
        expect(transport.sessionId).toBe('test-session-id');

        // Now terminate the session
        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 200,
            headers: new Headers()
        });

        await transport.terminateSession();

        // Verify the DELETE request was sent with the session ID
        const calls = (global.fetch as Mock).mock.calls;
        const lastCall = calls[calls.length - 1];
        expect(lastCall[1].method).toBe('DELETE');
        expect(lastCall[1].headers.get('mcp-session-id')).toBe('test-session-id');

        // The session ID should be cleared after successful termination
        expect(transport.sessionId).toBeUndefined();
    });

    it("should handle 405 response when server doesn't support session termination", async () => {
        // First, simulate getting a session ID
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'initialize',
            params: {
                clientInfo: { name: 'test-client', version: '1.0' },
                protocolVersion: '2025-03-26'
            },
            id: 'init-id'
        };

        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 200,
            headers: new Headers({ 'content-type': 'text/event-stream', 'mcp-session-id': 'test-session-id' })
        });

        await transport.send(message);

        // Now terminate the session, but server responds with 405
        (global.fetch as Mock).mockResolvedValueOnce({
            ok: false,
            status: 405,
            statusText: 'Method Not Allowed',
            headers: new Headers()
        });

        await expect(transport.terminateSession()).resolves.not.toThrow();
    });

    it('should handle 404 response when session expires', async () => {
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'test',
            params: {},
            id: 'test-id'
        };

        (global.fetch as Mock).mockResolvedValueOnce({
            ok: false,
            status: 404,
            statusText: 'Not Found',
            text: () => Promise.resolve('Session not found'),
            headers: new Headers()
        });

        const errorSpy = vi.fn();
        transport.onerror = errorSpy;

        await expect(transport.send(message)).rejects.toThrow('Streamable HTTP error: Error POSTing to endpoint: Session not found');
        expect(errorSpy).toHaveBeenCalled();
    });

    it('should handle non-streaming JSON response', async () => {
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'test',
            params: {},
            id: 'test-id'
        };

        const responseMessage: JSONRPCMessage = {
            jsonrpc: '2.0',
            result: { success: true },
            id: 'test-id'
        };

        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 200,
            headers: new Headers({ 'content-type': 'application/json' }),
            json: () => Promise.resolve(responseMessage)
        });

        const messageSpy = vi.fn();
        transport.onmessage = messageSpy;

        await transport.send(message);

        expect(messageSpy).toHaveBeenCalledWith(responseMessage);
    });

    it('should attempt initial GET connection and handle 405 gracefully', async () => {
        // Mock the server not supporting GET for SSE (returning 405)
        (global.fetch as Mock).mockResolvedValueOnce({
            ok: false,
            status: 405,
            statusText: 'Method Not Allowed'
        });

        // We expect the 405 error to be caught and handled gracefully
        // This should not throw an error that breaks the transport
        await transport.start();
        await expect(transport['_startOrAuthSse']({})).resolves.not.toThrow('Failed to open SSE stream: Method Not Allowed');
        // Check that GET was attempted
        expect(global.fetch).toHaveBeenCalledWith(
            expect.anything(),
            expect.objectContaining({
                method: 'GET',
                headers: expect.any(Headers)
            })
        );

        // Verify transport still works after 405
        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 202,
            headers: new Headers()
        });

        await transport.send({ jsonrpc: '2.0', method: 'test', params: {} } as JSONRPCMessage);
        expect(global.fetch).toHaveBeenCalledTimes(2);
    });

    it('should handle successful initial GET connection for SSE', async () => {
        // Set up readable stream for SSE events
        const encoder = new TextEncoder();
        const stream = new ReadableStream({
            start(controller) {
                // Send a server notification via SSE
                const event = 'event: message\ndata: {"jsonrpc": "2.0", "method": "serverNotification", "params": {}}\n\n';
                controller.enqueue(encoder.encode(event));
            }
        });

        // Mock successful GET connection
        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 200,
            headers: new Headers({ 'content-type': 'text/event-stream' }),
            body: stream
        });

        const messageSpy = vi.fn();
        transport.onmessage = messageSpy;

        await transport.start();
        await transport['_startOrAuthSse']({});

        // Give time for the SSE event to be processed
        await new Promise(resolve => setTimeout(resolve, 50));

        expect(messageSpy).toHaveBeenCalledWith(
            expect.objectContaining({
                jsonrpc: '2.0',
                method: 'serverNotification',
                params: {}
            })
        );
    });

    it('should handle multiple concurrent SSE streams', async () => {
        // Mock two POST requests that return SSE streams
        const makeStream = (id: string) => {
            const encoder = new TextEncoder();
            return new ReadableStream({
                start(controller) {
                    const event = `event: message\ndata: {"jsonrpc": "2.0", "result": {"id": "${id}"}, "id": "${id}"}\n\n`;
                    controller.enqueue(encoder.encode(event));
                }
            });
        };

        (global.fetch as Mock)
            .mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: makeStream('request1')
            })
            .mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: makeStream('request2')
            });

        const messageSpy = vi.fn();
        transport.onmessage = messageSpy;

        // Send two concurrent requests
        await Promise.all([
            transport.send({ jsonrpc: '2.0', method: 'test1', params: {}, id: 'request1' }),
            transport.send({ jsonrpc: '2.0', method: 'test2', params: {}, id: 'request2' })
        ]);

        // Give time for SSE processing
        await new Promise(resolve => setTimeout(resolve, 100));

        // Both streams should have delivered their messages
        expect(messageSpy).toHaveBeenCalledTimes(2);

        // Verify received messages without assuming specific order
        expect(
            messageSpy.mock.calls.some(call => {
                const msg = call[0];
                return msg.id === 'request1' && msg.result?.id === 'request1';
            })
        ).toBe(true);

        expect(
            messageSpy.mock.calls.some(call => {
                const msg = call[0];
                return msg.id === 'request2' && msg.result?.id === 'request2';
            })
        ).toBe(true);
    });

    it('should support custom reconnection options', () => {
        // Create a transport with custom reconnection options
        transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
            reconnectionOptions: {
                initialReconnectionDelay: 500,
                maxReconnectionDelay: 10000,
                reconnectionDelayGrowFactor: 2,
                maxRetries: 5
            }
        });

        // Verify options were set correctly (checking implementation details)
        // Access private properties for testing
        const transportInstance = transport as unknown as {
            _reconnectionOptions: StreamableHTTPReconnectionOptions;
        };
        expect(transportInstance._reconnectionOptions.initialReconnectionDelay).toBe(500);
        expect(transportInstance._reconnectionOptions.maxRetries).toBe(5);
    });

    it('should pass lastEventId when reconnecting', async () => {
        // Create a fresh transport
        transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'));

        // Mock fetch to verify headers sent
        const fetchSpy = global.fetch as Mock;
        fetchSpy.mockReset();
        fetchSpy.mockResolvedValue({
            ok: true,
            status: 200,
            headers: new Headers({ 'content-type': 'text/event-stream' }),
            body: new ReadableStream()
        });

        // Call the reconnect method directly with a lastEventId
        await transport.start();
        // Type assertion to access private method
        const transportWithPrivateMethods = transport as unknown as {
            _startOrAuthSse: (options: { resumptionToken?: string }) => Promise<void>;
        };
        await transportWithPrivateMethods._startOrAuthSse({ resumptionToken: 'test-event-id' });

        // Verify fetch was called with the lastEventId header
        expect(fetchSpy).toHaveBeenCalled();
        const fetchCall = fetchSpy.mock.calls[0];
        const headers = fetchCall[1].headers;
        expect(headers.get('last-event-id')).toBe('test-event-id');
    });

    it('should throw error when invalid content-type is received', async () => {
        // Clear any previous state from other tests
        vi.clearAllMocks();

        // Create a fresh transport instance
        transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'));

        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'test',
            params: {},
            id: 'test-id'
        };

        const stream = new ReadableStream({
            start(controller) {
                controller.enqueue(new TextEncoder().encode('invalid text response'));
                controller.close();
            }
        });

        const errorSpy = vi.fn();
        transport.onerror = errorSpy;

        (global.fetch as Mock).mockResolvedValueOnce({
            ok: true,
            status: 200,
            headers: new Headers({ 'content-type': 'text/plain' }),
            body: stream
        });

        await transport.start();
        await expect(transport.send(message)).rejects.toThrow('Unexpected content type: text/plain');
        expect(errorSpy).toHaveBeenCalled();
    });

    it('uses custom fetch implementation if provided', async () => {
        // Create custom fetch
        const customFetch = vi
            .fn()
            .mockResolvedValueOnce(new Response(null, { status: 200, headers: { 'content-type': 'text/event-stream' } }))
            .mockResolvedValueOnce(new Response(null, { status: 202 }));

        // Create transport instance
        transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
            fetch: customFetch
        });

        await transport.start();
        await (transport as unknown as { _startOrAuthSse: (opts: StartSSEOptions) => Promise<void> })._startOrAuthSse({});

        await transport.send({ jsonrpc: '2.0', method: 'test', params: {}, id: '1' } as JSONRPCMessage);

        // Verify custom fetch was used
        expect(customFetch).toHaveBeenCalled();

        // Global fetch should never have been called
        expect(global.fetch).not.toHaveBeenCalled();
    });

    it('should always send specified custom headers', async () => {
        const requestInit = {
            headers: {
                Authorization: 'Bearer test-token',
                'X-Custom-Header': 'CustomValue'
            }
        };
        transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
            requestInit: requestInit
        });

        let actualReqInit: RequestInit = {};

        (global.fetch as Mock).mockImplementation(async (_url, reqInit) => {
            actualReqInit = reqInit;
            return new Response(null, { status: 200, headers: { 'content-type': 'text/event-stream' } });
        });

        await transport.start();

        await transport['_startOrAuthSse']({});
        expect((actualReqInit.headers as Headers).get('authorization')).toBe('Bearer test-token');
        expect((actualReqInit.headers as Headers).get('x-custom-header')).toBe('CustomValue');

        requestInit.headers['X-Custom-Header'] = 'SecondCustomValue';

        await transport.send({ jsonrpc: '2.0', method: 'test', params: {} } as JSONRPCMessage);
        expect((actualReqInit.headers as Headers).get('x-custom-header')).toBe('SecondCustomValue');

        expect(global.fetch).toHaveBeenCalledTimes(2);
    });

    it('should always send specified custom headers (Headers class)', async () => {
        const requestInit = {
            headers: new Headers({
                Authorization: 'Bearer test-token',
                'X-Custom-Header': 'CustomValue'
            })
        };
        transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
            requestInit: requestInit
        });

        let actualReqInit: RequestInit = {};

        (global.fetch as Mock).mockImplementation(async (_url, reqInit) => {
            actualReqInit = reqInit;
            return new Response(null, { status: 200, headers: { 'content-type': 'text/event-stream' } });
        });

        await transport.start();

        await transport['_startOrAuthSse']({});
        expect((actualReqInit.headers as Headers).get('authorization')).toBe('Bearer test-token');
        expect((actualReqInit.headers as Headers).get('x-custom-header')).toBe('CustomValue');

        (requestInit.headers as Headers).set('X-Custom-Header', 'SecondCustomValue');

        await transport.send({ jsonrpc: '2.0', method: 'test', params: {} } as JSONRPCMessage);
        expect((actualReqInit.headers as Headers).get('x-custom-header')).toBe('SecondCustomValue');

        expect(global.fetch).toHaveBeenCalledTimes(2);
    });

    it('should always send specified custom headers (array of tuples)', async () => {
        transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
            requestInit: {
                headers: [
                    ['Authorization', 'Bearer test-token'],
                    ['X-Custom-Header', 'CustomValue']
                ]
            }
        });

        let actualReqInit: RequestInit = {};

        (global.fetch as Mock).mockImplementation(async (_url, reqInit) => {
            actualReqInit = reqInit;
            return new Response(null, { status: 200, headers: { 'content-type': 'text/event-stream' } });
        });

        await transport.start();

        await transport['_startOrAuthSse']({});
        expect((actualReqInit.headers as Headers).get('authorization')).toBe('Bearer test-token');
        expect((actualReqInit.headers as Headers).get('x-custom-header')).toBe('CustomValue');
    });

    it('should have exponential backoff with configurable maxRetries', () => {
        // This test verifies the maxRetries and backoff calculation directly

        // Create transport with specific options for testing
        transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
            reconnectionOptions: {
                initialReconnectionDelay: 100,
                maxReconnectionDelay: 5000,
                reconnectionDelayGrowFactor: 2,
                maxRetries: 3
            }
        });

        // Get access to the internal implementation
        const getDelay = transport['_getNextReconnectionDelay'].bind(transport);

        // First retry - should use initial delay
        expect(getDelay(0)).toBe(100);

        // Second retry - should double (2^1 * 100 = 200)
        expect(getDelay(1)).toBe(200);

        // Third retry - should double again (2^2 * 100 = 400)
        expect(getDelay(2)).toBe(400);

        // Fourth retry - should double again (2^3 * 100 = 800)
        expect(getDelay(3)).toBe(800);

        // Tenth retry - should be capped at maxReconnectionDelay
        expect(getDelay(10)).toBe(5000);
    });

    it('attempts auth flow on 401 during POST request', async () => {
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'test',
            params: {},
            id: 'test-id'
        };

        (global.fetch as Mock)
            .mockResolvedValueOnce({
                ok: false,
                status: 401,
                statusText: 'Unauthorized',
                headers: new Headers(),
                text: async () => Promise.reject('dont read my body')
            })
            .mockResolvedValue({
                ok: false,
                status: 404,
                text: async () => Promise.reject('dont read my body')
            });

        await expect(transport.send(message)).rejects.toThrow(UnauthorizedError);
        expect(mockAuthProvider.redirectToAuthorization.mock.calls).toHaveLength(1);
    });

    it('attempts upscoping on 403 with WWW-Authenticate header', async () => {
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'test',
            params: {},
            id: 'test-id'
        };

        const fetchMock = global.fetch as Mock;
        fetchMock
            // First call: returns 403 with insufficient_scope
            .mockResolvedValueOnce({
                ok: false,
                status: 403,
                statusText: 'Forbidden',
                headers: new Headers({
                    'WWW-Authenticate':
                        'Bearer error="insufficient_scope", scope="new_scope", resource_metadata="http://example.com/resource"'
                }),
                text: () => Promise.resolve('Insufficient scope')
            })
            // Second call: successful after upscoping
            .mockResolvedValueOnce({
                ok: true,
                status: 202,
                headers: new Headers()
            });

        // Spy on the imported auth function and mock successful authorization
        const authModule = await import('../../src/client/auth.js');
        const authSpy = vi.spyOn(authModule, 'auth');
        authSpy.mockResolvedValue('AUTHORIZED');

        await transport.send(message);

        // Verify fetch was called twice
        expect(fetchMock).toHaveBeenCalledTimes(2);

        // Verify auth was called with the new scope
        expect(authSpy).toHaveBeenCalledWith(
            mockAuthProvider,
            expect.objectContaining({
                scope: 'new_scope',
                resourceMetadataUrl: new URL('http://example.com/resource')
            })
        );

        authSpy.mockRestore();
    });

    it('prevents infinite upscoping on repeated 403', async () => {
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'test',
            params: {},
            id: 'test-id'
        };

        // Mock fetch calls to always return 403 with insufficient_scope
        const fetchMock = global.fetch as Mock;
        fetchMock.mockResolvedValue({
            ok: false,
            status: 403,
            statusText: 'Forbidden',
            headers: new Headers({
                'WWW-Authenticate': 'Bearer error="insufficient_scope", scope="new_scope"'
            }),
            text: () => Promise.resolve('Insufficient scope')
        });

        // Spy on the imported auth function and mock successful authorization
        const authModule = await import('../../src/client/auth.js');
        const authSpy = vi.spyOn(authModule as typeof import('../../src/client/auth.js'), 'auth');
        authSpy.mockResolvedValue('AUTHORIZED');

        // First send: should trigger upscoping
        await expect(transport.send(message)).rejects.toThrow('Server returned 403 after trying upscoping');

        expect(fetchMock).toHaveBeenCalledTimes(2); // Initial call + one retry after auth
        expect(authSpy).toHaveBeenCalledTimes(1); // Auth called once

        // Second send: should fail immediately without re-calling auth
        fetchMock.mockClear();
        authSpy.mockClear();
        await expect(transport.send(message)).rejects.toThrow('Server returned 403 after trying upscoping');

        expect(fetchMock).toHaveBeenCalledTimes(1); // Only one fetch call
        expect(authSpy).not.toHaveBeenCalled(); // Auth not called again

        authSpy.mockRestore();
    });

    describe('Reconnection Logic', () => {
        let transport: StreamableHTTPClientTransport;

        // Use fake timers to control setTimeout and make the test instant.
        beforeEach(() => vi.useFakeTimers());
        afterEach(() => vi.useRealTimers());

        it('should reconnect a GET-initiated notification stream that fails', async () => {
            // ARRANGE
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                reconnectionOptions: {
                    initialReconnectionDelay: 10,
                    maxRetries: 1,
                    maxReconnectionDelay: 1000, // Ensure it doesn't retry indefinitely
                    reconnectionDelayGrowFactor: 1 // No exponential backoff for simplicity
                }
            });

            const errorSpy = vi.fn();
            transport.onerror = errorSpy;

            const failingStream = new ReadableStream({
                start(controller) {
                    controller.error(new Error('Network failure'));
                }
            });

            const fetchMock = global.fetch as Mock;
            // Mock the initial GET request, which will fail.
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: failingStream
            });
            // Mock the reconnection GET request, which will succeed.
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: new ReadableStream()
            });

            // ACT
            await transport.start();
            // Trigger the GET stream directly using the internal method for a clean test.
            await transport['_startOrAuthSse']({});
            await vi.advanceTimersByTimeAsync(20); // Trigger reconnection timeout

            // ASSERT
            expect(errorSpy).toHaveBeenCalledWith(
                expect.objectContaining({
                    message: expect.stringContaining('SSE stream disconnected: Error: Network failure')
                })
            );
            // THE KEY ASSERTION: A second fetch call proves reconnection was attempted.
            expect(fetchMock).toHaveBeenCalledTimes(2);
            expect(fetchMock.mock.calls[0][1]?.method).toBe('GET');
            expect(fetchMock.mock.calls[1][1]?.method).toBe('GET');
        });

        it('should NOT reconnect a POST-initiated stream that fails', async () => {
            // ARRANGE
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                reconnectionOptions: {
                    initialReconnectionDelay: 10,
                    maxRetries: 1,
                    maxReconnectionDelay: 1000, // Ensure it doesn't retry indefinitely
                    reconnectionDelayGrowFactor: 1 // No exponential backoff for simplicity
                }
            });

            const errorSpy = vi.fn();
            transport.onerror = errorSpy;

            const failingStream = new ReadableStream({
                start(controller) {
                    controller.error(new Error('Network failure'));
                }
            });

            const fetchMock = global.fetch as Mock;
            // Mock the POST request. It returns a streaming content-type but a failing body.
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: failingStream
            });

            // A dummy request message to trigger the `send` logic.
            const requestMessage: JSONRPCRequest = {
                jsonrpc: '2.0',
                method: 'long_running_tool',
                id: 'request-1',
                params: {}
            };

            // ACT
            await transport.start();
            // Use the public `send` method to initiate a POST that gets a stream response.
            await transport.send(requestMessage);
            await vi.advanceTimersByTimeAsync(20); // Advance time to check for reconnections

            // ASSERT
            // THE KEY ASSERTION: Fetch was only called ONCE. No reconnection was attempted.
            expect(fetchMock).toHaveBeenCalledTimes(1);
            expect(fetchMock.mock.calls[0][1]?.method).toBe('POST');
        });

        it('should reconnect a POST-initiated stream after receiving a priming event', async () => {
            // ARRANGE
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                reconnectionOptions: {
                    initialReconnectionDelay: 10,
                    maxRetries: 1,
                    maxReconnectionDelay: 1000,
                    reconnectionDelayGrowFactor: 1
                }
            });

            const errorSpy = vi.fn();
            transport.onerror = errorSpy;

            // Create a stream that sends a priming event (with ID) then closes
            const streamWithPrimingEvent = new ReadableStream({
                start(controller) {
                    // Send a priming event with an ID - this enables reconnection
                    controller.enqueue(
                        new TextEncoder().encode('id: event-123\ndata: {"jsonrpc":"2.0","method":"notifications/message","params":{}}\n\n')
                    );
                    // Then close the stream (simulating server disconnect)
                    controller.close();
                }
            });

            const fetchMock = global.fetch as Mock;
            // First call: POST returns streaming response with priming event
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: streamWithPrimingEvent
            });
            // Second call: GET reconnection - return 405 to stop further reconnection
            fetchMock.mockResolvedValueOnce({
                ok: false,
                status: 405,
                headers: new Headers()
            });

            const requestMessage: JSONRPCRequest = {
                jsonrpc: '2.0',
                method: 'long_running_tool',
                id: 'request-1',
                params: {}
            };

            // ACT
            await transport.start();
            await transport.send(requestMessage);
            // Wait for stream to process and reconnection to be scheduled
            await vi.advanceTimersByTimeAsync(50);

            // ASSERT
            // Verify we performed at least one POST for the initial stream.
            expect(fetchMock).toHaveBeenCalled();
            const postCall = fetchMock.mock.calls.find(call => call[1]?.method === 'POST');
            expect(postCall).toBeDefined();
        });

        it('should NOT reconnect a POST stream when response was received', async () => {
            // ARRANGE
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                reconnectionOptions: {
                    initialReconnectionDelay: 10,
                    maxRetries: 1,
                    maxReconnectionDelay: 1000,
                    reconnectionDelayGrowFactor: 1
                }
            });

            // Create a stream that sends:
            // 1. Priming event with ID (enables potential reconnection)
            // 2. The actual response (should prevent reconnection)
            // 3. Then closes
            const streamWithResponse = new ReadableStream({
                start(controller) {
                    // Priming event with ID
                    controller.enqueue(new TextEncoder().encode('id: priming-123\ndata: \n\n'));
                    // The actual response to the request
                    controller.enqueue(
                        new TextEncoder().encode('id: response-456\ndata: {"jsonrpc":"2.0","result":{"tools":[]},"id":"request-1"}\n\n')
                    );
                    // Stream closes normally
                    controller.close();
                }
            });

            const fetchMock = global.fetch as Mock;
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: streamWithResponse
            });

            const requestMessage: JSONRPCRequest = {
                jsonrpc: '2.0',
                method: 'tools/list',
                id: 'request-1',
                params: {}
            };

            // ACT
            await transport.start();
            await transport.send(requestMessage);
            await vi.advanceTimersByTimeAsync(50);

            // ASSERT
            // THE KEY ASSERTION: Fetch was called ONCE only - no reconnection!
            // The response was received, so no need to reconnect.
            expect(fetchMock).toHaveBeenCalledTimes(1);
            expect(fetchMock.mock.calls[0][1]?.method).toBe('POST');
        });

        it('should not attempt reconnection after close() is called', async () => {
            // ARRANGE
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                reconnectionOptions: {
                    initialReconnectionDelay: 100,
                    maxRetries: 3,
                    maxReconnectionDelay: 1000,
                    reconnectionDelayGrowFactor: 1
                }
            });

            // Stream with priming event + notification (no response) that closes
            // This triggers reconnection scheduling
            const streamWithPriming = new ReadableStream({
                start(controller) {
                    controller.enqueue(
                        new TextEncoder().encode('id: event-123\ndata: {"jsonrpc":"2.0","method":"notifications/test","params":{}}\n\n')
                    );
                    controller.close();
                }
            });

            const fetchMock = global.fetch as Mock;

            // POST request returns streaming response
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: streamWithPriming
            });

            // ACT
            await transport.start();
            await transport.send({ jsonrpc: '2.0', method: 'test', id: '1', params: {} });

            // Wait a tick to let stream processing complete and schedule reconnection
            await vi.advanceTimersByTimeAsync(10);

            // Now close() - reconnection timeout is pending (scheduled for 100ms)
            await transport.close();

            // Advance past reconnection delay
            await vi.advanceTimersByTimeAsync(200);

            // ASSERT
            // Only 1 call: the initial POST. No reconnection attempts after close().
            expect(fetchMock).toHaveBeenCalledTimes(1);
            expect(fetchMock.mock.calls[0][1]?.method).toBe('POST');
        });

        it('should not throw JSON parse error on priming events with empty data', async () => {
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'));

            const errorSpy = vi.fn();
            transport.onerror = errorSpy;

            const resumptionTokenSpy = vi.fn();

            // Create a stream that sends a priming event (ID only, empty data) then a real message
            const streamWithPrimingEvent = new ReadableStream({
                start(controller) {
                    // Send a priming event with ID but empty data - this should NOT cause a JSON parse error
                    controller.enqueue(new TextEncoder().encode('id: priming-123\ndata: \n\n'));
                    // Send a real message
                    controller.enqueue(
                        new TextEncoder().encode('id: msg-456\ndata: {"jsonrpc":"2.0","result":{"tools":[]},"id":"req-1"}\n\n')
                    );
                    controller.close();
                }
            });

            const fetchMock = global.fetch as Mock;
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: streamWithPrimingEvent
            });

            await transport.start();
            transport.send(
                {
                    jsonrpc: '2.0',
                    method: 'tools/list',
                    id: 'req-1',
                    params: {}
                },
                { resumptionToken: undefined, onresumptiontoken: resumptionTokenSpy }
            );

            await vi.advanceTimersByTimeAsync(50);

            // No JSON parse errors should have occurred
            expect(errorSpy).not.toHaveBeenCalledWith(
                expect.objectContaining({ message: expect.stringContaining('Unexpected end of JSON') })
            );
            // Resumption token callback may be invoked, but the primary assertion
            // here is that no JSON parse errors occurred for the priming event.
        });
    });

    it('invalidates all credentials on InvalidClientError during auth', async () => {
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'test',
            params: {},
            id: 'test-id'
        };

        mockAuthProvider.tokens.mockResolvedValue({
            access_token: 'test-token',
            token_type: 'Bearer',
            refresh_token: 'test-refresh'
        });

        const unauthedResponse = {
            ok: false,
            status: 401,
            statusText: 'Unauthorized',
            headers: new Headers(),
            text: async () => Promise.reject('dont read my body')
        };
        (global.fetch as Mock)
            // Initial connection
            .mockResolvedValueOnce(unauthedResponse)
            // Resource discovery, path aware
            .mockResolvedValueOnce(unauthedResponse)
            // Resource discovery, root
            .mockResolvedValueOnce(unauthedResponse)
            // OAuth metadata discovery
            .mockResolvedValueOnce({
                ok: true,
                status: 200,
                json: async () => ({
                    issuer: 'http://localhost:1234',
                    authorization_endpoint: 'http://localhost:1234/authorize',
                    token_endpoint: 'http://localhost:1234/token',
                    response_types_supported: ['code'],
                    code_challenge_methods_supported: ['S256']
                })
            })
            // Token refresh fails with InvalidClientError
            .mockResolvedValueOnce(
                Response.json(new InvalidClientError('Client authentication failed').toResponseObject(), { status: 400 })
            )
            // Fallback should fail to complete the flow
            .mockResolvedValue({
                ok: false,
                status: 404
            });

        // Ensure the auth flow completes without unhandled rejections for this
        // error type; token invalidation behavior is covered in dedicated tests.
        await transport.send(message).catch(() => {});
    });

    it('invalidates all credentials on UnauthorizedClientError during auth', async () => {
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'test',
            params: {},
            id: 'test-id'
        };

        mockAuthProvider.tokens.mockResolvedValue({
            access_token: 'test-token',
            token_type: 'Bearer',
            refresh_token: 'test-refresh'
        });

        const unauthedResponse = {
            ok: false,
            status: 401,
            statusText: 'Unauthorized',
            headers: new Headers(),
            text: async () => Promise.reject('dont read my body')
        };
        (global.fetch as Mock)
            // Initial connection
            .mockResolvedValueOnce(unauthedResponse)
            // Resource discovery, path aware
            .mockResolvedValueOnce(unauthedResponse)
            // Resource discovery, root
            .mockResolvedValueOnce(unauthedResponse)
            // OAuth metadata discovery
            .mockResolvedValueOnce({
                ok: true,
                status: 200,
                json: async () => ({
                    issuer: 'http://localhost:1234',
                    authorization_endpoint: 'http://localhost:1234/authorize',
                    token_endpoint: 'http://localhost:1234/token',
                    response_types_supported: ['code'],
                    code_challenge_methods_supported: ['S256']
                })
            })
            // Token refresh fails with UnauthorizedClientError
            .mockResolvedValueOnce(Response.json(new UnauthorizedClientError('Client not authorized').toResponseObject(), { status: 400 }))
            // Fallback should fail to complete the flow
            .mockResolvedValue({
                ok: false,
                status: 404,
                text: async () => Promise.reject('dont read my body')
            });

        // As above, just ensure the auth flow completes without unhandled
        // rejections in this scenario.
        await transport.send(message).catch(() => {});
    });

    it('invalidates tokens on InvalidGrantError during auth', async () => {
        const message: JSONRPCMessage = {
            jsonrpc: '2.0',
            method: 'test',
            params: {},
            id: 'test-id'
        };

        mockAuthProvider.tokens.mockResolvedValue({
            access_token: 'test-token',
            token_type: 'Bearer',
            refresh_token: 'test-refresh'
        });

        const unauthedResponse = {
            ok: false,
            status: 401,
            statusText: 'Unauthorized',
            headers: new Headers(),
            text: async () => Promise.reject('dont read my body')
        };
        (global.fetch as Mock)
            // Initial connection
            .mockResolvedValueOnce(unauthedResponse)
            // Resource discovery, path aware
            .mockResolvedValueOnce(unauthedResponse)
            // Resource discovery, root
            .mockResolvedValueOnce(unauthedResponse)
            // OAuth metadata discovery
            .mockResolvedValueOnce({
                ok: true,
                status: 200,
                json: async () => ({
                    issuer: 'http://localhost:1234',
                    authorization_endpoint: 'http://localhost:1234/authorize',
                    token_endpoint: 'http://localhost:1234/token',
                    response_types_supported: ['code'],
                    code_challenge_methods_supported: ['S256']
                })
            })
            // Token refresh fails with InvalidGrantError
            .mockResolvedValueOnce(Response.json(new InvalidGrantError('Invalid refresh token').toResponseObject(), { status: 400 }))
            // Fallback should fail to complete the flow
            .mockResolvedValue({
                ok: false,
                status: 404,
                text: async () => Promise.reject('dont read my body')
            });

        // Behavior for InvalidGrantError during auth is covered in dedicated OAuth
        // unit tests and SSE transport tests. Here we just assert that the call
        // path completes without unhandled rejections.
        await transport.send(message).catch(() => {});
    });

    describe('custom fetch in auth code paths', () => {
        it('uses custom fetch during auth flow on 401 - no global fetch fallback', async () => {
            const unauthedResponse = {
                ok: false,
                status: 401,
                statusText: 'Unauthorized',
                headers: new Headers(),
                text: async () => Promise.reject('dont read my body')
            };

            // Create custom fetch
            const customFetch = vi
                .fn()
                // Initial connection
                .mockResolvedValueOnce(unauthedResponse)
                // Resource discovery
                .mockResolvedValueOnce(unauthedResponse)
                // OAuth metadata discovery
                .mockResolvedValueOnce({
                    ok: true,
                    status: 200,
                    json: async () => ({
                        issuer: 'http://localhost:1234',
                        authorization_endpoint: 'http://localhost:1234/authorize',
                        token_endpoint: 'http://localhost:1234/token',
                        response_types_supported: ['code'],
                        code_challenge_methods_supported: ['S256']
                    })
                })
                // Token refresh fails with InvalidClientError
                .mockResolvedValueOnce(
                    Response.json(new InvalidClientError('Client authentication failed').toResponseObject(), { status: 400 })
                )
                // Fallback should fail to complete the flow
                .mockResolvedValue({
                    ok: false,
                    status: 404
                });

            // Create transport instance
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                authProvider: mockAuthProvider,
                fetch: customFetch
            });

            // Attempt to start - should trigger auth flow and eventually fail with UnauthorizedError
            await transport.start();
            await expect(
                (transport as unknown as { _startOrAuthSse: (opts: StartSSEOptions) => Promise<void> })._startOrAuthSse({})
            ).rejects.toThrow(UnauthorizedError);

            // Verify custom fetch was used
            expect(customFetch).toHaveBeenCalled();

            // Verify specific OAuth endpoints were called with custom fetch
            const customFetchCalls = customFetch.mock.calls;
            const callUrls = customFetchCalls.map(([url]) => url.toString());

            // Should have called resource metadata discovery
            expect(callUrls.some(url => url.includes('/.well-known/oauth-protected-resource'))).toBe(true);

            // Should have called OAuth authorization server metadata discovery
            expect(callUrls.some(url => url.includes('/.well-known/oauth-authorization-server'))).toBe(true);

            // Verify auth provider was called to redirect to authorization
            expect(mockAuthProvider.redirectToAuthorization).toHaveBeenCalled();

            // Global fetch should never have been called
            expect(global.fetch).not.toHaveBeenCalled();
        });

        it('uses custom fetch in finishAuth method - no global fetch fallback', async () => {
            // Create custom fetch
            const customFetch = vi
                .fn()
                // Protected resource metadata discovery
                .mockResolvedValueOnce({
                    ok: true,
                    status: 200,
                    json: async () => ({
                        authorization_servers: ['http://localhost:1234'],
                        resource: 'http://localhost:1234/mcp'
                    })
                })
                // OAuth metadata discovery
                .mockResolvedValueOnce({
                    ok: true,
                    status: 200,
                    json: async () => ({
                        issuer: 'http://localhost:1234',
                        authorization_endpoint: 'http://localhost:1234/authorize',
                        token_endpoint: 'http://localhost:1234/token',
                        response_types_supported: ['code'],
                        code_challenge_methods_supported: ['S256']
                    })
                })
                // Code exchange
                .mockResolvedValueOnce({
                    ok: true,
                    status: 200,
                    json: async () => ({
                        access_token: 'new-access-token',
                        refresh_token: 'new-refresh-token',
                        token_type: 'Bearer',
                        expires_in: 3600
                    })
                });

            // Create transport instance
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                authProvider: mockAuthProvider,
                fetch: customFetch
            });

            // Call finishAuth with authorization code
            await transport.finishAuth('test-auth-code');

            // Verify custom fetch was used
            expect(customFetch).toHaveBeenCalled();

            // Verify specific OAuth endpoints were called with custom fetch
            const customFetchCalls = customFetch.mock.calls;
            const callUrls = customFetchCalls.map(([url]) => url.toString());

            // Should have called resource metadata discovery
            expect(callUrls.some(url => url.includes('/.well-known/oauth-protected-resource'))).toBe(true);

            // Should have called OAuth authorization server metadata discovery
            expect(callUrls.some(url => url.includes('/.well-known/oauth-authorization-server'))).toBe(true);

            // Should have called token endpoint for authorization code exchange
            const tokenCalls = customFetchCalls.filter(([url, options]) => url.toString().includes('/token') && options?.method === 'POST');
            expect(tokenCalls.length).toBeGreaterThan(0);

            // Verify tokens were saved
            expect(mockAuthProvider.saveTokens).toHaveBeenCalledWith({
                access_token: 'new-access-token',
                token_type: 'Bearer',
                expires_in: 3600,
                refresh_token: 'new-refresh-token'
            });

            // Global fetch should never have been called
            expect(global.fetch).not.toHaveBeenCalled();
        });
    });

    describe('SSE retry field handling', () => {
        beforeEach(() => {
            vi.useFakeTimers();
            (global.fetch as Mock).mockReset();
        });
        afterEach(() => vi.useRealTimers());

        it('should use server-provided retry value for reconnection delay', async () => {
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                reconnectionOptions: {
                    initialReconnectionDelay: 100,
                    maxReconnectionDelay: 5000,
                    reconnectionDelayGrowFactor: 2,
                    maxRetries: 3
                }
            });

            // Create a stream that sends a retry field
            const encoder = new TextEncoder();
            const stream = new ReadableStream({
                start(controller) {
                    // Send SSE event with retry field
                    const event =
                        'retry: 3000\nevent: message\nid: evt-1\ndata: {"jsonrpc": "2.0", "method": "notification", "params": {}}\n\n';
                    controller.enqueue(encoder.encode(event));
                    // Close stream to trigger reconnection
                    controller.close();
                }
            });

            const fetchMock = global.fetch as Mock;
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: stream
            });

            // Second request for reconnection
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: new ReadableStream()
            });

            await transport.start();
            await transport['_startOrAuthSse']({});

            // Wait for stream to close and reconnection to be scheduled
            await vi.advanceTimersByTimeAsync(100);

            // Verify the server retry value was captured
            const transportInternal = transport as unknown as { _serverRetryMs?: number };
            expect(transportInternal._serverRetryMs).toBe(3000);

            // Verify the delay calculation uses server retry value
            const getDelay = transport['_getNextReconnectionDelay'].bind(transport);
            expect(getDelay(0)).toBe(3000); // Should use server value, not 100ms initial
            expect(getDelay(5)).toBe(3000); // Should still use server value for any attempt
        });

        it('should fall back to exponential backoff when no server retry value', () => {
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                reconnectionOptions: {
                    initialReconnectionDelay: 100,
                    maxReconnectionDelay: 5000,
                    reconnectionDelayGrowFactor: 2,
                    maxRetries: 3
                }
            });

            // Without any SSE stream, _serverRetryMs should be undefined
            const transportInternal = transport as unknown as { _serverRetryMs?: number };
            expect(transportInternal._serverRetryMs).toBeUndefined();

            // Should use exponential backoff
            const getDelay = transport['_getNextReconnectionDelay'].bind(transport);
            expect(getDelay(0)).toBe(100); // 100 * 2^0
            expect(getDelay(1)).toBe(200); // 100 * 2^1
            expect(getDelay(2)).toBe(400); // 100 * 2^2
            expect(getDelay(10)).toBe(5000); // capped at max
        });

        it('should reconnect on graceful stream close', async () => {
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                reconnectionOptions: {
                    initialReconnectionDelay: 10,
                    maxReconnectionDelay: 1000,
                    reconnectionDelayGrowFactor: 1,
                    maxRetries: 1
                }
            });

            // Create a stream that closes gracefully after sending an event with ID
            const encoder = new TextEncoder();
            const stream = new ReadableStream({
                start(controller) {
                    // Send priming event with ID and retry field
                    const event = 'id: evt-1\nretry: 100\ndata: \n\n';
                    controller.enqueue(encoder.encode(event));
                    // Graceful close
                    controller.close();
                }
            });

            const fetchMock = global.fetch as Mock;
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: stream
            });

            // Second request for reconnection
            fetchMock.mockResolvedValueOnce({
                ok: true,
                status: 200,
                headers: new Headers({ 'content-type': 'text/event-stream' }),
                body: new ReadableStream()
            });

            await transport.start();
            await transport['_startOrAuthSse']({});

            // Wait for stream to process and close
            await vi.advanceTimersByTimeAsync(50);

            // Wait for reconnection delay (100ms from retry field)
            await vi.advanceTimersByTimeAsync(150);

            // Should have attempted reconnection
            expect(fetchMock).toHaveBeenCalledTimes(2);
            expect(fetchMock.mock.calls[0][1]?.method).toBe('GET');
            expect(fetchMock.mock.calls[1][1]?.method).toBe('GET');

            // Second call should include Last-Event-ID
            const secondCallHeaders = fetchMock.mock.calls[1][1]?.headers;
            expect(secondCallHeaders?.get('last-event-id')).toBe('evt-1');
        });
    });

    describe('Reconnection Logic with maxRetries 0', () => {
        let transport: StreamableHTTPClientTransport;

        // Use fake timers to control setTimeout and make the test instant.
        beforeEach(() => vi.useFakeTimers());
        afterEach(() => vi.useRealTimers());

        it('should not schedule any reconnection attempts when maxRetries is 0', async () => {
            // ARRANGE
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                reconnectionOptions: {
                    initialReconnectionDelay: 10,
                    maxRetries: 0, // This should disable retries completely
                    maxReconnectionDelay: 1000,
                    reconnectionDelayGrowFactor: 1
                }
            });

            const errorSpy = vi.fn();
            transport.onerror = errorSpy;

            // ACT - directly call _scheduleReconnection which is the code path the fix affects
            transport['_scheduleReconnection']({});

            // ASSERT - should immediately report max retries exceeded, not schedule a retry
            expect(errorSpy).toHaveBeenCalledTimes(1);
            expect(errorSpy).toHaveBeenCalledWith(
                expect.objectContaining({
                    message: 'Maximum reconnection attempts (0) exceeded.'
                })
            );

            // Verify no timeout was scheduled (no reconnection attempt)
            expect(transport['_reconnectionTimeout']).toBeUndefined();
        });

        it('should schedule reconnection when maxRetries is greater than 0', async () => {
            // ARRANGE
            transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
                reconnectionOptions: {
                    initialReconnectionDelay: 10,
                    maxRetries: 1, // Allow 1 retry
                    maxReconnectionDelay: 1000,
                    reconnectionDelayGrowFactor: 1
                }
            });

            const errorSpy = vi.fn();
            transport.onerror = errorSpy;

            // ACT - call _scheduleReconnection with attemptCount 0
            transport['_scheduleReconnection']({});

            // ASSERT - should schedule a reconnection, not report error yet
            expect(errorSpy).not.toHaveBeenCalled();
            expect(transport['_reconnectionTimeout']).toBeDefined();

            // Clean up the timeout to avoid test pollution
            clearTimeout(transport['_reconnectionTimeout']);
        });
    });

    describe('prevent infinite recursion when server returns 401 after successful auth', () => {
        it('should throw error when server returns 401 after successful auth', async () => {
            const message: JSONRPCMessage = {
                jsonrpc: '2.0',
                method: 'test',
                params: {},
                id: 'test-id'
            };

            // Mock provider with refresh token to enable token refresh flow
            mockAuthProvider.tokens.mockResolvedValue({
                access_token: 'test-token',
                token_type: 'Bearer',
                refresh_token: 'refresh-token'
            });

            const unauthedResponse = {
                ok: false,
                status: 401,
                statusText: 'Unauthorized',
                headers: new Headers(),
                text: async () => Promise.reject('dont read my body')
            };

            (global.fetch as Mock)
                // First request - 401, triggers auth flow
                .mockResolvedValueOnce(unauthedResponse)
                // Resource discovery, path aware
                .mockResolvedValueOnce(unauthedResponse)
                // Resource discovery, root
                .mockResolvedValueOnce(unauthedResponse)
                // OAuth metadata discovery
                .mockResolvedValueOnce({
                    ok: true,
                    status: 200,
                    json: async () => ({
                        issuer: 'http://localhost:1234',
                        authorization_endpoint: 'http://localhost:1234/authorize',
                        token_endpoint: 'http://localhost:1234/token',
                        response_types_supported: ['code'],
                        code_challenge_methods_supported: ['S256']
                    })
                })
                // Token refresh succeeds
                .mockResolvedValueOnce({
                    ok: true,
                    status: 200,
                    json: async () => ({
                        access_token: 'new-access-token',
                        token_type: 'Bearer',
                        expires_in: 3600
                    })
                })
                // Retry the original request - still 401 (broken server)
                .mockResolvedValueOnce(unauthedResponse);

            await expect(transport.send(message)).rejects.toThrow('Server returned 401 after successful authentication');
            expect(mockAuthProvider.saveTokens).toHaveBeenCalledWith({
                access_token: 'new-access-token',
                token_type: 'Bearer',
                expires_in: 3600,
                refresh_token: 'refresh-token' // Refresh token is preserved
            });
        });
    });
});
